#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 时间     : 2018-12-06 17:03:57
# 作者     : DL (584054432@qq.com)
# 网址     : https://gitee.com/dingliangPython/
# 软件版本 : Python3.6.5
# 功能     ： PLC连接,读，写，解析数据

import re,snap7,struct
from collections import Iterable
class S7Parser(object):
    """[summary]

    [s7 协议读写PLC 数据封装]
    """
    def __init__(self,conn=None,log=print):
        """
            conn:plc连接对像
        """
        super(S7Parser, self).__init__()
        self.conn=conn
        self.log=log
    #连接到PLC
    def connect(self,ip,rack,slot,iTSAP=None,rTSAP=None,port=102):
        '''[summary]

        [创建PLC连接对象]

        Args:
            ip: [plc的IP地址]
            rack: [板卡号]
            slot: [插槽号]
            iTSAP: [本地TSAP] (default: {None})
            rTSAP: [远程TSAP] (default: {None})
            port: [端口] (default: {102})

        Returns:
            [Clinet类对象]
        '''
        try :
            client= snap7.client.Client()
            if iTSAP and rTSAP:
                client.set_connection_params(ip, iTSAP, rTSAP)
            client.connect(ip,rack,slot,port)
            return client
            self.log.info("连接到PLC,ip:%s,rack:%s,slot:%s,iTSAP:%#x,rTSAP:%#x，成功"%(ip,rack,slot,iTSAP,rTSAP))
        except Exception as e:
            self.log.error("连接到PLC,ip:%s,rack:%s,slot:%s,iTSAP:%#x,rTSAP:%#x，失败，请检测连接配置及网络通信"%(ip,rack,slot,iTSAP,rTSAP))
    #读取PLC数据
    def read(self,address,size=None,dataType=None):
        """[summary]

        [功能：读取PLC数据]

        Args:
           address: [PLC寄存器地址,如，I0.1]
           size: [对占用一个字节以上有效] (default: {None})
           dataType: [plc数据类型  bool 1bit 布尔,
                                    byte 8bit 字节,
                                    word 16bit 字,
                                    dword 32bit 双字,
                                    char 8bit 字符,
                                    short 16bit 短整型,
                                    dint  32bit 长整型,
                                    real  32bit 浮点整 ] (default: {None})

        Returns:
           [返回读取数据，bool,类型：真=2，假=1]
           [type]
        """
        if not self.conn:
            self.log.error("PLC连接不正常，不能读取PLC数据")
            return
        #数据类型
        dataType=dataType.lower() if dataType else None
        Type=address.upper()[0]
        a= re.findall('\d+',address)
        begin=int(a[0])
        #IQMVDB偏移位
        exc=int(a[1]) if len(a)>=2 else None
        #DB块，110.100.1 的1
        bits=int(a[2]) if len(a)>=3 else None
        try:
            if Type=='I':
                data=self.conn.read_area(0x81,0,begin,size if size else 1)
                data=data if size else data[0]
                value=self.parser(data,'bool')
                return value if size else value[exc]
            elif Type=='Q':
                data=self.conn.read_area(0x82,0,begin,size if size else 1)
                data=data if size else data[0]
                value=self.parser(data,'bool')
                return value if size else value[exc]
            elif Type=='M':
                data=self.conn.read_area(0x83,0,begin,size if size else 1)
                data=data if size else data[0]
                value=self.parser(data,'bool')
                return value if size else value[exc]
            elif Type=='V':
                data=self.conn.read_area(0x84,1,begin,size if size else 1)
                value=self.parser(data,dataType)
                return value if size else value[exc]
            elif Type=='D':
                data=self.conn.read_area(area=0x84,dbnumber=begin,start=exc,size=size if size else 1)
                if bits:
                    return self.parser(data,'bool')[bits]
                else:
                    return self.parser(data,dataType)
        except Exception as e:
            self.log.warning("读取PLC异常：%s"%e)
    #读取plc数据，返回字节数据
    def read_v2(self,address,size=1):
        Type=address.upper()[0]
        area=0x81 if Type=='I' else 0x82 if Type=='Q' else 0x83 if Type=='M' else 0x84
        a= re.findall('\d+',address)
        begin=int(a[0])
            #IQMVDB偏移位
        exc=int(a[1]) if len(a)>=2 else None
        if Type in 'IQMV':
            data=self.conn.read_area(area,1,begin,size)
            return data[0] if size==1 else data
        elif Type =='D':
            data=self.conn.read_area(area,begin,exc,size)
            return data[0] if size==1 else data
    #写入PLC数据
    def write(self,address,data,dataType=None):
        """[summary]

        [功能：写数据到PLC]
        注:写单个bit时，1=False 非1=True
        Args:
            address: [PLC寄存器地址,如，I0.1]
            data: [要写入的数据，连接地址数据用列表或元组，字符串不变]
            dataType: [[plc数据类型  bool 1bit 布尔,
                                    byte 8bit 字节,
                                    word 16bit 字,
                                    dword 32bit 双字,
                                    char 8bit 字符,
                                    short 16bit 短整型,
                                    dint  32bit 长整型,
                                    real  32bit 浮点整 ] (default: {None})

        Returns:
            [写入成功=Ture]

        例：
            write('I1',123,'byte') 写整个I1
            write('I1.0',2,'bool') 写I1的，0位bit位
            write('DB1.0','help','char') 写DB1,偏移0开始，DB1.0-DB1.3
            write('DB1.0',[1,2,3,4],'byte') 写DB1, 偏移0开始，DB1.0-DB1.3
        """
        a= re.findall('\d+',address)
        begin=int(a[0])
        #IQMVDB偏移位
        exc=int(a[1]) if len(a)>=2 else None
        #DB块，110.100.1 的1
        bits=int(a[2]) if len(a)>=3 else None
        #寄存器类型
        Type=address.upper()[0]
        #区块代码
        area=0x81 if Type=='I' else 0x82 if Type=='Q' else 0x83 if Type=='M' else 0x84
        if not self.conn:
            self.log.warning("PLC连接不正常，检测连接")
            return None
        try:
            #写bit位相间
            if dataType=='bool' or not dataType:
                self.log.info("检测是写整个byte还是bit，IQM,偏移位：%s,VDB,偏移位：%s,写值：%s"%(exc,bits,data))
                if data<=2 and (Type in 'IQMV' and type(exc)==type(1)) or type(bits)==type(1):
                    bit=bits if type(bits)==type(1) else exc
                    #读数据
                    value=self.read_v2(address)
                    #如果值已是1，不再执行写入
                    if [2 if value&2**i!=0 else 1 for i in range(8)] [bit]!=1 or data!=1:
                        #1为关，2为开 与关，或开
                        v= value&(value-2**bit) if data==1 else value|(2**bit)
                        #打包
                        fmt=struct.pack('!B',v )
                        begin=1 if Type in 'IQMV' else begin
                        exc= begin if Type in 'IQMV' else exc
                        rec=self.conn.write_area(area,begin,exc,fmt)
                elif Type in 'IQMV' and not exc:
                    fmt=begin,struct.pack('!B',data)
                    self.conn.write_area(area,1,begin,fmt)
                elif Type=='D' and not bits :
                    fmt=struct.pack('!B',data)
                    self.conn.write_area(area,begin,exc,fmt)
            #转换成可迭代对像
            data=[ v for v in data] if isinstance(data,Iterable) else [data]
            #数量
            i=len(data)
            #写byte
            if dataType=='byte':
                fmt=struct.pack('!%sB'%i,*data)
                begin=1 if Type in 'IQMV' else begin
                exc= begin if Type in 'IQMV' else exc
                self.conn.write_area(area,begin,exc,fmt)
            #写字
            elif dataType=='word':
                fmt=struct.pack('!%sH'%i,*data)
                begin=1 if Type in 'IQMV' else begin
                exc= begin if Type in 'IQMV' else exc
                self.conn.write_area(area,begin,exc,fmt)
            #双字
            elif dataType=='dword':
                fmt=struct.pack('!%sI'%i,*data)
                begin=1 if Type in 'IQMV' else begin
                exc= begin if Type in 'IQMV' else exc
                self.conn.write_area(area,begin,exc,fmt)
            #字符
            elif dataType=='char' or dataType=='string':
                data=[ v.encode('utf-8') for v in data]
                fmt=struct.pack('!%sc'%i,*data)
                begin=1 if Type in 'IQMV' else begin
                exc= begin if Type in 'IQMV' else exc
                self.conn.write_area(area,begin,exc,fmt)
            elif dataType=='short':
                fmt=struct.pack('!%sh'%i,*data)
                begin=1 if Type in 'IQMV' else begin
                exc= begin if Type in 'IQMV' else exc
                self.conn.write_area(area,begin,exc,fmt)
            elif dataType=='dint' or dataType=='int':
                fmt=struct.pack('!%sI'%i,*data)
                begin=1 if Type in 'IQMV' else begin
                exc= begin if Type in 'IQMV' else exc
                self.conn.write_area(area,begin,exc,fmt)
            elif dataType=='real' or dataType=='float':
                fmt=struct.pack('!%sf'%i,*data)
                begin=1 if Type in 'IQMV' else begin
                exc= begin if Type in 'IQMV' else exc
                self.conn.write_area(area,begin,exc,fmt)
            self.log.info("写入 %s,数据：%s,类型：%s,写入成功"%(address,data,dataType))
            return True
        except Exception as e:
            self.log.warning("写入 %s,数据：%s,类型：%s,失败:%s"%(address,data,dataType,e))
            return False
    #解析plc数据
    def parser(self,data,dataType='original'):
        """[summary]

        [功能：解析二进制字节流]

        Args:
        data: [PLC读取的字节流]
        dataType: [  bool 1bit 布尔,
                    byte 8bit 字节,
                    word 16bit 字,
                    dword 32bit 双字,
                    char 8bit 字符,
                    short 16bit 短整型,
                    dint  32bit 长整型,
                    real  32bit 浮点整 ]

        Returns:
        [返回解析后数据]
        [type]
        """
        dataType=dataType.lower()
        if dataType=='bool':
            if type(data)==type(bytearray(b'')):
                return [[2 if v&2**i!=0 else 1 for i in range(8)]  for v in data]
            else:
                return [2 if data&2**i!=0 else 1 for i in range(8)]
        elif dataType=='byte':
            number=len(data) if type(data)==type(bytearray(b'')) else 1
            value=struct.unpack('!%dB'%number,data) if type(data)==type(bytearray(b'')) else data
            return value if number==1 else value[0]
        elif dataType=='word':
            number=len(data)/2
            value=struct.unpack('!%dH'%number,data)
            return value if number>1 else value[0]
        elif dataType=='dword':
            number=len(data)/4
            value=struct.unpack('!%di'%number,data)
            return value if number>1 else value[0]
        elif dataType=='char' or dataType=='string':
            number=len(data) if type(data)==type(bytearray(b'')) else 1
            value=struct.unpack('!%dc'%number,data) if type(data)==type(bytearray(b'')) else chr(data) if type(data)!=type(1) else data
            value=[str(v,'utf-8').replace('\x00','')  for v in value] if type(value)==type('') else chr(value)
            return value if number>1 else value[0]
        elif dataType=='short':
            number=len(data)/2
            value=struct.unpack('!%dh'%number,data)
            return value if number>1 else value[0]
        elif dataType=='dint' or dataType=='int':
            number=len(data)/4
            value=struct.unpack('!%dI'%number,data)
            return value if number>1 else value[0]
        elif dataType=='real' or dataType=='float':
            number=len(data)/4
            value=struct.unpack('!%df'%number,data)
            return value if number>1 else value[0]
        elif dataType=='original' :
            value=[ chr(v) for v in data if v>31]
            value=''.join(value)
            return value
    def parser_v2(self,address,data,dataType):
        #数据类型
        dataType=dataType.lower() if dataType else None
        Type=address.upper()[0]
        a= re.findall('\d+',address)
        begin=int(a[0])
        #IQMVDB偏移位
        exc=int(a[1]) if len(a)>=2 else None
        #DB块，110.100.1 的1
        bits=int(a[2]) if len(a)>=3 else None
        size=1 if dataType in( 'bool','byte','char','string') else 2 if dataType in ('word','short') else 4
        begin=begin if address[0]!='D' else  int(a[1])
        newData=  data[begin] if size==1 else data[begin:begin+size] if dataType!='original' or not dataType else data
        data=self.parser(newData,dataType)
        data =data[bits] if type(bits)==type(1) else data[exc] if address[0]!='D' and  type(exc)==type(1) else data
        return data
